/*
 * Copyright 2000-2005 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jetbrains.idea.accurev.accurev.connections;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.process.InterruptibleActivity;
import com.intellij.openapi.vcs.VcsException;
import com.jniwrapper.*;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.idea.accurev.AccuRevBundle;
import org.jetbrains.idea.accurev.accurev.ExecResult;
import org.jetbrains.idea.accurev.accurev.P4Callback;
import org.jetbrains.idea.accurev.accurev.AccuRevSettings;
import org.jetbrains.idea.accurev.accurev.PerforceTimeoutException;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author akireyev
 */
class P4NativeConnection extends Structure {
  @NonNls private static final String P4_LIB_NAME = "p4api";
  private static final Library P4_LIB = new Library(P4_LIB_NAME);
  private static final Logger LOG = Logger.getInstance("#org.jetbrains.idea.accurev.accurev.connections.P4NativeConnection");

  private static class StringPointer extends Pointer{
    public StringPointer(AnsiString parameter) {
      super(parameter);
    }

    public AnsiString getReferencedObject() {
      return (AnsiString)super.getReferencedObject();
    }
  }

  private final StringPointer myErrorPtr;
  private final StringPointer myPortPtr;
  private final StringPointer myClientPtr;
  private final StringPointer myUserPtr;
  private final StringPointer myPasswordPtr;
  private final StringPointer myCharsetPtr;
  private final StringPointer myWorkingDirPtr;

  private P4Callback myOutCallback;
  private P4Callback myErrCallback;

  private final Pointer mySelfPointer;

  private static final Object P4_NATIVE_LOCK = new Object();

  @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"})
  private volatile boolean myNativeCrashed = false;

  @NonNls private static final String CREATE_CONNECTION_METHOD = "createConnection";
  @NonNls private static final String INIT_CONNECTION_METHOD = "initConnection";
  @NonNls private static final String RUN_COMMAND_METHOD = "runCommand";

  public P4NativeConnection() {
    synchronized (P4_NATIVE_LOCK) {
      myPortPtr = new StringPointer(new AnsiString());
      myClientPtr = new StringPointer(new AnsiString());
      myUserPtr = new StringPointer(new AnsiString());
      myPasswordPtr = new StringPointer(new AnsiString());
      myErrorPtr = new StringPointer(new AnsiString());
      myCharsetPtr = new StringPointer(new AnsiString());
      myWorkingDirPtr = new StringPointer(new AnsiString());
      init(new Parameter[]{myPortPtr, myClientPtr, myUserPtr, myPasswordPtr, myCharsetPtr,
        myWorkingDirPtr, new Pointer.Void(), myErrorPtr}, (short)8);
      Function createConn = P4_LIB.getFunction(CREATE_CONNECTION_METHOD);
      createConn.setCallingConvention(Function.CDECL_CALLING_CONVENTION);
      createConn.invoke(this);
      mySelfPointer = new Pointer(this);
      myOutCallback = new P4Callback();
      myErrCallback = new P4Callback();
    }
  }

  public void init() throws VcsException {
    synchronized (P4_NATIVE_LOCK) {
      Function initConn = P4_LIB.getFunction(INIT_CONNECTION_METHOD);
      initConn.setCallingConvention(Function.CDECL_CALLING_CONVENTION);
      initConn.invoke(null, mySelfPointer);
      checkError();
    }
  }

  public void setPort(String port) {
    synchronized (P4_NATIVE_LOCK) {
      setValue(myPortPtr, port);
    }
  }

  private static void setValue(final StringPointer pointer, final String stringValue) {
    if (stringValue != null && stringValue.length() > 0) {
      pointer.setNull(false);
      pointer.getReferencedObject().setValue(stringValue);
    }
    else {
      pointer.setNull(true);
    }
  }

  public void setClient(String client) {
    synchronized(P4_NATIVE_LOCK) {
      setValue(myClientPtr, client);
    }
  }

  public void setUser(String user) {
    synchronized(P4_NATIVE_LOCK) {
      setValue(myUserPtr, user);
    }
  }

  public void setPassword(String password) {
    synchronized (P4_NATIVE_LOCK) {
      setValue(myPasswordPtr, password);
    }
  }

  private void checkError() throws VcsException {
    if (!myErrorPtr.isNull()) {
      String errorMessage = myErrorPtr.getReferencedObject().getValue();
      throw new VcsException(AccuRevBundle.message("exception.text.p4.error", errorMessage));
    }
  }

  public void runP4Command(final AccuRevSettings settings,
                           final String[] p4args,
                           final ExecResult retVal
  )
    throws VcsException, PerforceTimeoutException {
    final int timeout = settings.SERVER_TIMEOUT;

    synchronized (P4_NATIVE_LOCK) {
      final Function run = P4_LIB.getFunction(RUN_COMMAND_METHOD);
      run.setCallingConvention(Function.CDECL_CALLING_CONVENTION);
      ComplexArray argv;
      final Parameter pargv;
      final int argc;
      if (p4args.length > 1) {
        List<Parameter> args = new ArrayList<Parameter>(p4args.length);
        for (int i = 1; i < p4args.length; i++) {
          String p4arg = p4args[i];
          if (p4arg != null) {
            args.add(new Pointer(new AnsiString(p4args[i])));
          }
        }
        argv = new ComplexArray(args.toArray(new Parameter[args.size()]));
        pargv = new Pointer(argv);
        argc = args.size();
      }
      else {
        pargv = new Pointer.Void(0);
        argc = 0;
      }

      final P4Callback outCallback = myOutCallback;
      final P4Callback errCallback = myErrCallback;

      outCallback.reset();
      errCallback.reset();

      final boolean needStallDialog = StallConnectionUtil.needDialog();
      InterruptibleActivity worker = new InterruptibleActivity(timeout, TimeUnit.MILLISECONDS) {
        protected void start() {
          try {
            myErrorPtr.setNull(true);
            run.invoke(null, new Parameter[]{
              mySelfPointer,
              new Pointer(new AnsiString(p4args[0])),
              new Int(argc),
              pargv,
              outCallback,
              errCallback});
            retVal.setStdout(outCallback.getText());
            retVal.setStderr(errCallback.getText());
            retVal.setByteOut(outCallback.getBytes());

          }
          catch (FunctionExecutionException e) {
            LOG.error(e);
            myNativeCrashed = true;
          }
          finally {
            outCallback.deactivate();
            errCallback.deactivate();
          }
        }

        protected void interrupt() {
        }

        protected int processTimeout() {
          return StallConnectionUtil.requestUser();
        }

        @Override
        protected int processTimeoutInEDT() {
          if (!needStallDialog || settings.getProject().isDisposed()) {
            return -2;
          }
          return super.processTimeoutInEDT();
        }
      };

      outCallback.setExecutionWorker(worker);
      errCallback.setExecutionWorker(worker);

      final int rc = worker.execute();
      if (myOutCallback.isActive() || myErrCallback.isActive()) {
        myOutCallback = new P4Callback();
        myErrCallback = new P4Callback();
      }

      if (rc != 0) {
        throw new PerforceTimeoutException();
      }

      checkError();
      //noinspection NonPrivateFieldAccessedInSynchronizedContext
      retVal.setExitCode(0);
    }
  }

  public boolean hasNativeCrashed() {
    return myNativeCrashed;
  }

  public void setCharset(final String charset) {
    synchronized (P4_NATIVE_LOCK) {
      setValue(myCharsetPtr, charset);
    }
  }

  public void setWorkingDir(final String workingDir) {
    synchronized (P4_NATIVE_LOCK) {
      setValue(myWorkingDirPtr, workingDir);
    }
  }
}
